package cn.sylinx.horm.resource.io;

import java.io.File;
import java.io.IOException;
import java.net.JarURLConnection;
import java.net.URL;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import cn.sylinx.horm.exception.HORMException;
import cn.sylinx.horm.util.StrKit;

public abstract class BaseResourceScanner {

    private final String packageName;
    private final String packageNameWithDot;
    private final String packageDirName;
    private final String packagePath;

    private final Set<Object> resources = new HashSet<>();

    public BaseResourceScanner() {
        this(null);
    }

    public BaseResourceScanner(String packageName) {
        packageName = nullToEmpty(packageName);
        this.packageName = packageName;
        this.packageNameWithDot = packageName.endsWith(".") ? packageName : packageName.concat(".");
        this.packageDirName = packageName.replace('.', File.separatorChar);
        this.packagePath = packageName.replace('.', '/');
    }

    public Set<Object> scan() {
        return scan(false);
    }

    private static Enumeration<URL> getResourceIter(String resource) {
        final Enumeration<URL> resources;
        try {
            resources = Resources.getClassLoader().getResources(resource);
        } catch (IOException e) {
            throw new HORMException(e);
        }
        return resources;
    }

    public Set<Object> scan(boolean forceScanJavaClassPaths) {

        Enumeration<URL> eurls = getResourceIter(this.packagePath);

        while (eurls.hasMoreElements()) {

            URL url = eurls.nextElement();

            switch (url.getProtocol()) {
                case "file":
                    scanFile(new File(url.getFile()), null);
                    break;
                case "jar":
                    scanJar(getJarFile(url));
                    break;
            }
        }

        if (forceScanJavaClassPaths || this.resources.isEmpty()) {
            scanJavaClassPaths();
        }

        return Collections.unmodifiableSet(this.resources);
    }

    private JarFile getJarFile(URL url) {
        try {
            JarURLConnection urlConnection = (JarURLConnection) url.openConnection();
            return urlConnection.getJarFile();
        } catch (IOException e) {
            throw new HORMException(e);
        }
    }

    private void scanJavaClassPaths() {
        final String[] javaClassPaths = getJavaClassPaths();
        for (String classPath : javaClassPaths) {
            scanFile(new File(classPath), null);
        }
    }

    private String[] getJavaClassPaths() {
        return System.getProperty("java.class.path").split(File.pathSeparator);
    }

    protected abstract String getMatchedPostfix();

    protected boolean accept(String resoureName) {
        return true;
    }

    private void scanFile(File file, String rootDir) {

        String postfix = getMatchedPostfix();
        int postfixLen = postfix.length();

        if (file.isFile()) {
            final String fileName = file.getAbsolutePath();
            if (fileName.endsWith(postfix)) {
                final String resourceName = fileName.substring(rootDir.length(), fileName.length() - postfixLen)//
                        .replace(File.separatorChar, '.');//
                addIfAccept(resourceName);
            } else if (fileName.endsWith(".jar")) {
                try {
                    scanJar(new JarFile(file));
                } catch (IOException e) {
                    throw new HORMException(e);
                }
            }
        } else if (file.isDirectory()) {
            final File[] files = file.listFiles();
            if (null != files) {
                for (File subFile : files) {
                    scanFile(subFile, (null == rootDir) ? subPathBeforePackage(file) : rootDir);
                }
            }
        }
    }

    private void scanJar(JarFile jar) {

        String postfix = getMatchedPostfix();
        int postfixLen = postfix.length();

        String name;
        Enumeration<JarEntry> jarEntrys = jar.entries();
        while (jarEntrys.hasMoreElements()) {
            JarEntry entry = jarEntrys.nextElement();
            name = removePrefix(entry.getName(), "/");
            if (isEmpty(packagePath) || name.startsWith(this.packagePath)) {
                if (name.endsWith(postfix) && !entry.isDirectory()) {
                    final String resourceName = name//
                            .substring(0, name.length() - postfixLen)//
                            .replace('/', '.');//
                    addIfAccept(resourceName);
                }
            }
        }
    }

    private boolean isEmpty(CharSequence str) {
        return str == null || str.length() == 0;
    }

    private String removePrefix(String str, CharSequence prefix) {
        if (isEmpty(str) || isEmpty(prefix)) {
            return str;
        }

        if (str.startsWith(prefix.toString())) {
            return subSuf(str, prefix.length());// 截取后半段
        }
        return str;
    }

    private String subSuf(String string, int fromIndex) {
        if (isEmpty(string)) {
            return null;
        }
        return sub(string, fromIndex, string.length());
    }

    private String sub(String str, int fromIndexInclude, int toIndexExclude) {
        if (isEmpty(str)) {
            return str;
        }
        int len = str.length();

        if (fromIndexInclude < 0) {
            fromIndexInclude = len + fromIndexInclude;
            if (fromIndexInclude < 0) {
                fromIndexInclude = 0;
            }
        } else if (fromIndexInclude > len) {
            fromIndexInclude = len;
        }

        if (toIndexExclude < 0) {
            toIndexExclude = len + toIndexExclude;
            if (toIndexExclude < 0) {
                toIndexExclude = len;
            }
        } else if (toIndexExclude > len) {
            toIndexExclude = len;
        }

        if (toIndexExclude < fromIndexInclude) {
            int tmp = fromIndexInclude;
            fromIndexInclude = toIndexExclude;
            toIndexExclude = tmp;
        }

        if (fromIndexInclude == toIndexExclude) {
            return "";
        }

        return str.substring(fromIndexInclude, toIndexExclude);
    }

    private void addIfAccept(String resoureName) {
        if (StrKit.isBlank(resoureName)) {
            return;
        }
        int resoureLen = resoureName.length();
        int packageLen = this.packageName.length();
        if (resoureLen == packageLen) {
            if (resoureName.equals(this.packageName)) {
                addIfAcceptByFilterIgnoreException(resoureName);
            }
        } else if (resoureLen > packageLen) {
            if (".".equals(this.packageNameWithDot) || resoureName.startsWith(this.packageNameWithDot)) {
                addIfAcceptByFilterIgnoreException(resoureName);
            }
        }
    }

    private void addIfAcceptByFilterIgnoreException(String resoureName) {

        try {
            addIfAcceptByFilter(resoureName);
        } catch (Exception e) {
            // ignore
        }
    }

    private void addIfAcceptByFilter(String resoureName) {

        if (null != resoureName) {
            if (accept(resoureName)) {
                this.resources.add(transform(resoureName));
            }
        }
    }

    protected Object transform(String resoureName) {
        return resoureName;
    }

    private String subPathBeforePackage(File file) {
        String filePath = file.getAbsolutePath();
        if (isNotEmpty(this.packageDirName)) {
            filePath = subBefore(filePath, this.packageDirName, true);
        }

        if (filePath == null) {
            throw new HORMException("file path must not be null");
        }

        if (filePath.endsWith(File.separator)) {
            return filePath;
        }

        return filePath.concat(File.separator);
    }

    private boolean isNotEmpty(String str) {
        return !isEmpty(str);
    }

    private String subBefore(String str, String sep, boolean isLastSeparator) {

        if (isEmpty(str) || sep == null) {
            return str;
        }

        if (sep.isEmpty()) {
            return "";
        }

        final int pos = isLastSeparator ? str.lastIndexOf(sep) : str.indexOf(sep);
        if (-1 == pos) {
            return str;
        }
        if (0 == pos) {
            return "";
        }
        return str.substring(0, pos);
    }

    private String nullToEmpty(String str) {
        return (str == null) ? "" : str;
    }
}
